<template>
	<view class="com-scan"
	:data-arrowColor="arrowColor"
	:style="{background, width, height}"
	:start="isStart" :change:start="scanRender.startWatcher"
	:play="isPlay" :change:play="scanRender.playWatcher"
	:image="imagePath" :change:image="scanRender.imageWatcher">
		<slot></slot>
		<view class="com-scan-bar" :class="{'com-scan-bar-animation': isStart == 1}" v-if="showScanBar">
			<view class="com-scan-line" :style="{
				'box-shadow': '0 0 15px ' + scanbarColor,
				'background-color': scanbarColor
			}"></view>
		</view>
	</view>
</template>

<script>
	export default {
		props: {
			//自动扫码
			auto: {
				type: Boolean,
				default: true
			},
			background: {
				type: String,
				default: '#000000'
			},
			showScanBar: {
				type: Boolean,
				default: true
			},
			scanbarColor: {
				type: String,
				default: 'rgb(0, 255, 0)'
			},
			arrowColor: {
				type: String,
				default: 'rgb(0, 255, 0)'
			},
			width: {
				type: String,
				default: '100vw'
			},
			height: {
				type: String,
				default: '100vh'
			}
		},
		data () {
			return {
				isStart: -1,
				isPlay: -1,
				imagePath: ''
			}
		},
		methods: {
			//开始扫码
			start () {
				this.isStart = -1
				this.$nextTick(() => {
					this.isStart = 1
				})
			},
			//停止扫码
			stop () {
				this.isStart = -1
				this.$nextTick(() => {
					this.isStart = 0
				})
			},
			//继续扫码
			play () {
				this.isPlay = -1
				this.$nextTick(() => {
					this.isPlay = 1
				})
			},
			//暂停扫码
			pause () {
				this.isPlay = -1
				this.$nextTick(() => {
					this.isPlay = 0
				})
			},
			//传入图片识别
			image (path) {
				this.imagePath = ''
				this.$nextTick(function () {
					this.imagePath = path
				})
			},
			//通过相册选择
			album () {
				uni.chooseImage({
				  count: 1, // 只选择一张图片
				  sizeType: ['original'], // 使用原图，识别成功率更高
				  sourceType: ['album'], // 只从相册选择
				  success: (res) => {
					//去识别
				    this.image(res.tempFilePaths[0])
				  }
				});
			},
			//销毁通知
			destroyed () {
				this.$emit('destroyed')
			},
			callResult (e) {
				this.isStart = -1
				this.$emit('scaned', e)
			},
			callReady () {
				this.$emit('ready')//通知外部组件已准备好
				if ( this.auto ) {
					this.start()
				}
			}
		}
	}
</script>

<script lang="renderjs" type="module" module="scanRender">
	import jsQR from "./jsQR.js"
	export default {
		data () {
			return {
				dom: null,
				video: null,
				canvas: null,
				context: null
			}
		},
		mounted () {
			this.dom = document.getElementsByClassName('com-scan')[0]
			this.callMethod('callReady', true)//通知逻辑层，渲染层已做好准备
		},
		beforeDestroy () {
			this.stopRender()
		},
		methods: {
			startWatcher (newVal) {
				if ( newVal == 1 ) {
					this.startRender()
				}
				if ( newVal == 0 ) {
					this.stopRender()
				}
			},
			playWatcher (newVal) {
				if ( newVal == 1 ) {
					this.removeArrow()
					this.video && this.video.play()
				}
				if ( newVal == 0 ) {
					this.video && this.video.pause()
				}
			},
			imageWatcher (newVal) {
				if ( newVal ) {
					if ( !this.canvas ) {
						this.canvas = document.createElement('canvas');
						this.context = this.canvas.getContext('2d');
					}
					const img = document.createElement('IMG')
					img.src = newVal
					img.crossOrigin = 'anonymous'
					img.onload = () => {
						this.canvas.width = img.width
						this.canvas.height = img.height
						this.context.drawImage(img, 0, 0, this.canvas.width, this.canvas.height); // 画布上的目标区域（全屏）
						// 获取图像数据
						const imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
						// 使用jsQR解析二维码
						const code = jsQR(imageData.data, imageData.width, imageData.height);
						if (code) {
							// 找到二维码，输出结果
							this.callMethod('callResult', code)
						} else {
							//未识别到二维码
							this.callMethod('callResult', {
								errMsg: '未识别到二维码',
								data: ''
							})
						}
					}
				}
			},
			//开始扫码
			startRender () {
				this.removeArrow()
				if ( this.video ) this.stopRender()//先停止一下，避免重复调用
				if ( 'navigator' in window && 'mediaDevices' in navigator ) {
					this.getUserMedia({ 
						video: { 
							facingMode: "environment",
							width: { min: 640, ideal: 1280, max: 1920 }, // 提供分辨率范围，避免ideal值过高:cite[3]
							height: { min: 480, ideal: 720, max: 1080 }
						},
						audio: false
					}, (stream) => {
						this.video = document.createElement('VIDEO')
						this.video.setAttribute('style', 'position:absolute;top:0;left:0;width:100%;height:100%;background-color:#000;object-fit:cover;')
						this.video.srcObject = stream;
						this.video.muted = true
						this.video.play();
						//设置属性，让video不被劫持
						this.video.setAttribute('x-webkit-airplay', 'allow');
						this.video.setAttribute('webkit-playsinline', true);
						this.video.setAttribute('playsinline', true);
						this.video.setAttribute('x5-video-player-type', 'h5');
						this.video.setAttribute('x5-video-play', true);
						this.video.setAttribute('poster', undefined)
						this.dom.appendChild(this.video)
						this.canvas = document.createElement('canvas');
						this.context = this.canvas.getContext('2d');
						requestAnimationFrame(this.tick);
					}, (err) => {
						const div = document.createElement('DIV');
						div.innerHTML = `
							<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;z-index:2;">无法访问摄像头${err.toString()}</div>
						`
						this.dom.appendChild(div)
					})
				}
			},
			//开始扫描
			tick() {
				if ( !this.video ) return
			    if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
			        // 设置canvas尺寸与视频一致
			        this.canvas.width = this.dom.offsetWidth;
			        this.canvas.height = this.dom.offsetHeight;
						  
					const videoWidth = this.video.videoWidth;
					const videoHeight = this.video.videoHeight;
					const canvasRatio = this.canvas.width / this.canvas.height;
					const videoRatio = videoWidth / videoHeight;
					let sourceX, sourceY, sourceWidth, sourceHeight;
					// 计算需要从视频中裁剪的部分，以保持屏幕宽高比
					if (videoRatio > canvasRatio) {
					  // 视频更宽，裁剪左右两边
					  sourceHeight = videoHeight;
					  sourceWidth = videoHeight * canvasRatio;
					  sourceX = (videoWidth - sourceWidth) / 2;
					  sourceY = 0;
					} else {
					  // 视频更高，裁剪上下两边
					  sourceWidth = videoWidth;
					  sourceHeight = videoWidth / canvasRatio;
					  sourceX = 0;
					  sourceY = (videoHeight - sourceHeight) / 2;
					}
					// 将裁剪后的视频帧绘制到与屏幕同尺寸的画布上
					this.context.drawImage(
					  this.video,
					  sourceX, sourceY, sourceWidth, sourceHeight, // 源图像上的裁剪区域
					  0, 0, this.canvas.width, this.canvas.height // 画布上的目标区域（全屏）
					);
			        // 获取图像数据
			        const imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
			        // 使用jsQR解析二维码
			        const code = jsQR(imageData.data, imageData.width, imageData.height);
			        if (code) {
						this.video.pause()
						this.pushArrow(code)
						// 找到二维码，输出结果
						this.callMethod('callResult', code)
			            // 可以在这里停止扫描，如果需要连续扫描则不需要停止
			            return;
			        } else {
						// console.log('未识别到二维码', code);
			        }
			    }
			    requestAnimationFrame(this.tick);
			},
			//添加箭头
			pushArrow (code) {
				const location = code.location
				const centerX = (location.topLeftCorner.x + location.topRightCorner.x + 
				location.bottomLeftCorner.x + location.bottomRightCorner.x) / 4;
				const centerY = (location.topLeftCorner.y + location.topRightCorner.y + 
				location.bottomLeftCorner.y + location.bottomRightCorner.y) / 4;
				const arrow = document.createElement('DIV')
				arrow.setAttribute('class', 'com-scan-arrow')
				arrow.setAttribute('style', `position:absolute;left:${centerX}px;top:${centerY}px;transform:translate(-50%, -50%);`)
				arrow.innerHTML = `
					<svg viewBox="0 0 1024 1024" width="30" height="30">
					<path d="M512 929.959184c-230.4 0-417.959184-187.559184-417.959184-417.959184s187.559184-417.959184 417.959184-417.959184 417.959184 187.559184 417.959184 417.959184-187.559184 417.959184-417.959184 417.959184z" fill="${this.dom.getAttribute('data-arrowColor')}" p-id="8100"></path>
					<path d="M444.081633 689.632653c-5.22449 0-10.44898-2.089796-14.628572-6.269388-8.359184-8.359184-8.359184-21.420408 0-29.779592l156.734694-156.734693c8.359184-8.359184 21.420408-8.359184 29.779592 0 8.359184 8.359184 8.359184 21.420408 0 29.779591l-156.734694 156.734694c-4.702041 4.179592-9.926531 6.269388-15.15102 6.269388z" fill="#DCFFFA" p-id="8101"></path>
					<path d="M600.816327 532.897959c-5.22449 0-10.44898-2.089796-14.628572-6.269388l-156.734694-156.734693c-8.359184-8.359184-8.359184-21.420408 0-29.779592 8.359184-8.359184 21.420408-8.359184 29.779592 0l156.734694 156.734694c8.359184 8.359184 8.359184 21.420408 0 29.779591-4.702041 4.179592-9.926531 6.269388-15.15102 6.269388z" fill="#DCFFFA" p-id="8102"></path>
					</svg>
				`
				this.dom.appendChild(arrow)
			},
			//移除箭头
			removeArrow () {
				const arrows = document.querySelectorAll('com-scan-arrow')
				arrows.forEach(a => a.remove())
			},
			//停止扫码
			stopRender () {
				this.canvas = null
				this.context = null
				if ( this.video ) {
					//停止视频流
					const stream = this.video.srcObject
					const tracks = stream.getTracks();
					tracks.forEach(track => {
					    track.stop()
					})
					this.video.removeAttribute('srcObject')
					this.video.load()
					this.video.remove()
					this.video = null
				}
				this.callMethod('destroyed')
			},
			getUserMedia(constraints, success, error) {
			  if (navigator.mediaDevices.getUserMedia) {
			    //最新的标准API
			    navigator.mediaDevices.getUserMedia(constraints).then(success).catch(error);
			  } else if (navigator.webkitGetUserMedia) {
			    //webkit核心浏览器
			    navigator.webkitGetUserMedia(constraints, success, error)
			  } else if (navigator.mozGetUserMedia) {
			    //firfox浏览器
			    navigator.mozGetUserMedia(constraints, success, error);
			  } else if (navigator.getUserMedia) {
			    //旧版API
			    navigator.getUserMedia(constraints, success, error);
			  } else {
				error && error('该设备不支持getUserMedia')
			  }
			},
			callMethod (name, args) {
				// #ifndef H5
				this.$ownerInstance.callMethod(name, args)
				// #endif
				// #ifdef H5
				this[name](args)
				// #endif
			}
		}
	}
</script>

<style>
	.com-scan {
		position: relative;
	}
	.com-scan-bar {
		position: absolute;
		width: 100%;
		height: 100%;
		top: 0;
		left: 0;
		z-index: 1;
		visibility: hidden;
	}
	.com-scan-bar-animation {
		visibility: visible;
		animation: scan 3s infinite linear;
	}
	.com-scan-line {
		position: absolute;
		width: 70%;
		height: 1px;
		top: 0;
		left: 50%;
		transform: translateX(-50%);
	}
	
	/* 扫描线动画 */
	@keyframes scan {
	    0% {
			opacity: 0;
	        transform: translateY(0);
	    }
		50% {
			opacity: 1;
			transform: translateY(50%);
		}
	    100% {
			opacity: 0;
	        transform: translateY(100%);
	    }
	}
</style>